β

Как стать автором
Погнали в космос?
Да случится мэтч: оцени совместимость с IT в ритейле

Моя лентаВсе потоки

0

Рейтинг

Karuna

Подписаться

varanio 2 июн 2021 в 13:30

Дженерики в языке Go

6 мин

70K

Блог компании KarunaGo*Программирование*

Технотекст 2021

func Map[F, T any](s []F, f func(F) T) []T {
r := make([]T, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}

Как вы уже наверняка знаете, proposal по дженерикам в Golang принят (официально это называется type parameters) и будет имплементирован в go 1.18. Бета будет доступна уже в конце этого года. А это значит, что пора разобраться, на чём в итоге остановились разработчики языка — ведь черновик type parameters постоянно менялся в течение последних лет.

Технология новая, на практике толком никто не использовал. Поэтому если увидите какую-то неточность в статье, не стесняйтесь указать это в комментариях.

Самостоятельно поиграться с дженериками можно здесь

Итак, поехали.

Зачем нужны дженерики в Go?

Несколько лет назад у нас в Каруне возникла необходимость перевести несколько сервисов с PHP на GO. Как сейчас помню, в первой же программе потребовалось проверить, существует ли строка в некотором слайсе строк. Беглый гуглёж показал, что встроенной функции, аналогичной пхпшной in_array() в языке нет. Поэтому пришлось написать свою, что-то типа такого:

func stringExistsInSlice(val string, values []string) bool {
for _, v := range values {
if val == v {
return true
}
}

return false
}

Но проблема в том, что когда надо поискать int в слайсе интов, функция получается абсолютно такой же, отличие только в сигнатуре.

func existsInSlice(val int, values []int) bool {
for _, v := range values {
if val == v {
return true
}
}

return false
}

Написать универсальную функцию под все типы — задача не очень простая. Можно использовать reflect и interface{}, как в примере на stackoverflow, но это, понятное дело, выглядит не очень и подвержено ошибкам, не проверяемым в момент компиляции. Или же можно использовать кодогенерацию, что тоже в общем-то так себе, так как это лишний шаг при билде.

Забегая вперёд, в go 1.18 это будет решаться так:

func existsInSlice[T comparable](val T, values []T) bool {
for _, v := range values {
if val == v {
return true
}
}

return false
}

Нужно ли усложнять язык дженериками?

Вопрос дискуссионный. Мнения разделились.

Как известно, язык Go изначально был заточен под максимальную простоту, и обобщение типов может усложнить читабельность кода. Многие противопоставляют Go языку Java, традиционно наполненному обобщениями различного рода, и дженерики — это как первый шаг в эту сторону. Теряется этакая гошная "дубовость" (в хорошем смысле), прямолинейность. Тем более, что в обычном продуктовом коде люди годами обходятся и без этого. Порой проще скопипастить немного кода и не париться. Как говорили в одном докладе, копипаста — это "хорошее медитативное занятие".

С другой стороны, если надо написать универсальную библиотеку для каких-то универсальных целей, то придётся использовать interface{} или кодогенерацию, а это тоже в общем-то читабельности и надёжности не добавляет. Также необходимо отметить, что разработчики языка сделали всё возможное, чтобы дженерики выглядели и использовались как можно проще. Намного проще, чем в других языках.

Согласно результатам опроса 88% респондентов назвали отсутствие дженериков критической проблемой. 18% опрошенных сказали, что не используют Go именно из-за отсутствия этой функциональности (цитата: "18% of respondents are prevented from using Go because of a lack of generics").

Синтаксис функции с type parameters

Вот простейший пример. Функция, построчно печатающая элементы слайса любого типа.

func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Println(v)
}
}

т.е. после имени функции в квадратных скобках описан некий идентификатор T (который дальше будет использован там, где вы бы раньше использовали обычный тип) и констрейнт (ограничение) для этого типа (констрейнт any означает, что в качестве T можно передать любой тип). Таких идентификаторов может быть несколько (через запятую); констрейнт для них указывать обязательно, это будет подробнее описано ниже.

А вот так происходит вызов такой функции, уже с конкретным типом string:

greetings := []string{"Hello", "world"};
PrintSlice[string](greetings)

Т.е. синтаксически по сути мы передаём в функцию тип как обычный аргумент (параметр). Просто такие "аргументы" передаются и описываются в сигнатуре в отдельных квадратных скобках вместо круглых. Поэтому функциональность так и называется: type parameters.

В некоторых случаях явным образом передавать тип не надо, компилятор его сможет вывести сам по переданным аргументам:

greetings := []string{"Hello", "world"};
PrintSlice(greetings)

Констрейнты (ограничения типов)

У каждого параметра-типа обязательно указывается ограничение типа.

func [T MyConstraint] (...

, где MyConstraint — это интерфейс, который описывает, каким может быть тип. Этот интерфейс может быть обычным go-интерфейсом, описывающим требуемые методы.

type MyConstraint interface {
String() string
}

А может быть интерфейсом, перечисляющим полный список типов, для которых он может быть использован.

type MyConstraint interface {
int | int8 | int16 | int32 | int64
}

Обратите внимание, что такой интерфейс с перечислением пока что можно использовать только в дженериках и нигде больше. В будущих версиях будут и другие применения.

Есть встроенные в язык констрейнты, например any (синоним interface{}) и comparable (ограничивающий типы, для которых определены операторы сравнения).

Также в стандартную библиотеку планируется добавить пакет constraints, где будут добавлены различные полезняшки. Например, constraints.Number (под это подходят любые типы а ля int, float32 и т.д. )

Типы с обобщениями

Помимо функций подобным образом можно работать и с описанием типа.

Например, если вы пишете функции для умножения и сложения векторов, вам захочется иметь универсальный тип Vector

type Vector[T constraints.Number] []T

При использовании такого типа нужно указать в квадратных скобках его конкретный уже тип:

var myVec Vector[int]

Вот более-менее полный пример функции сложения векторов

type Number interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}

type Vector[T Number] []T

func AddVector[T Number](vec1 Vector[T], vec2 Vector[T]) Vector[T] {
var result Vector[T]
for i := range vec1 {
result = append(result, vec1[i]+vec2[i])
}
return result
}

func main() {
v1 := Vector[int]{1, 2, 3}
v2 := Vector[int]{3, 4, 5}
result := AddVector(v1, v2)
fmt.Println(result)
}

поиграться с примером можно здесь: https://gotipplay.golang.org/p/UZ3GdScMwZC

(в примере я не использовал встроенный интерфейс constraints.Number, так как на go2goplay.golang.org это почему-то не работает. Пришлось делать свой доморощенный interface Number)

Обратите внимание на то, что здесь нельзя использовать констрейнт any, так как операция сложения определена далеко не для всех типов, и вы получите соответствующую ошибку "operator + not defined".

Некоторые замечания по реализации

Пакеты

В стандартную библиотеку планируется добавить несколько пакетов, таких как slices, maps, chans и т.д., которые будут предоставлять универсальные функции для работы со слайсами, каналами и т.д.

Пакеты container/list, container/ring, sync и другие будут доработаны с точки зрения типобезопасности. Math получит новые универсальные функции для любых чисел (например Min и Max)

Эффективность

Т.е. обобщенная функция будет компилироваться один раз и будет иметь некоторые потери в рантайме. В то же время обобщённые типы будут компилироваться для каждого варианта отдельно, что увеличит лишь время компиляции.

Цитата:

Generic functions, rather than generic types, can probably be compiled using an interface-based approach. That will optimize compile time, in that the function is only compiled once, but there will be some run time cost.

Generic types may most naturally be compiled multiple times for each set of type arguments. This will clearly carry a compile time cost, but there shouldn't be any run time cost. Compilers can also choose to implement generic types similarly to interface types, using special purpose methods to access each element that depends on a type parameter.

Отличие от java

Как известно, java удаляет информацию о дженериках после компиляции. В golang это не так.

Также, в Джаве реализована ковариантность и контрвариантность (List<? extends Number>, List<? super Number>), в Go всё намного проще: мы просто передаём тип как параметр, а тип ограничен интерфейсом.

Кстати, многие спрашивают, почему нельзя было сделать стандартные для многих языков (включая Java) скобки <>, а ввели новый вариант в виде квадратных скобок?

Дело в том, что парсер языка Go сделан максимально простым и быстрым — это ключевая особенность языка. В случае с треугольными скобками в некоторых ситуациях невозможно распарсить строку без знания информации о типах, например:

a, b = w < x, y > (z)

можно трактовать как

a, b = (w<x), (y<z)
//или
a, b = w<x,y> (z)

Ещё примеры

Типобезопасная функция, объединяющая два канала в один

func Merge[T any](c1, c2 <-chan T) <-chan T {
r := make(chan T)
go func(c1, c2 <-chan T, r chan<- T) {
defer close(r)
for c1 != nil || c2 != nil {
select {
case v1, ok := <-c1:
if ok {
r <- v1
} else {
c1 = nil
}
case v2, ok := <-c2:
if ok {
r <- v2
} else {
c2 = nil
}
}
}
}(c1, c2, r)
return r
}

Делаем свой Set на основе map

package sets

type Set[T comparable] map[T]struct{}

func Make[T comparable]() Set[T] {
return make(Set[T])
}

func (s Set[T]) Add(v T) {
s[v] = struct{}{}
}

func (s Set[T]) Delete(v T) {
delete(s, v)
}

func (s Set[T]) Contains(v T) bool {
_, ok := s[v]
return ok
}

func (s Set[T]) Len() int {
return len(s)
}

func (s Set[T]) Iterate(f func(T)) {
for v := range s {
f(v)
}
}

Пример использования

s := sets.Make[int]()
s.Add(1)
if s.Contains(2) { panic("unexpected 2") }

Что дальше?

Дальше — больше. Есть куча предложений, как улучшить дженерики: в частности автоматический вывод типов в различных ситуациях или упрощённая работа с zero value. Но это всё по сути ждёт, когда основная функциональность попадёт в язык.

В общем, поживём — увидим. Осталось уже недолго.

Теги:

golang

generics

Хабы:

Блог компании Karuna

Go

Программирование

+43

107

49
+49

Karuna
Компания

Подписаться

Сайт

277

Карма

0

Рейтинг

Антон Околелов @varanio

Go-тимлид, веду канал https://t.me/crossjoin

Подписаться

Хабр КарьераFacebookXTelegram

Комментарии 49
+49

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.7K

Кейс

Перевод

+27

20

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.4K

Мнение

+22

17

60
+60

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Показать еще

Минуточку внимания

Событие

Что ни день – то ивент, если следить за Календарём

Турбо

Делаем сисадмина сильнее с помощью менеджера паролей

Турбо

Полный свайп: оцени, подходит ли тебе работа в IT-ритейле

Курсы

Методы описания бизнес-процессов (IDEF, DFD, BPMN, EPC, UML)
7 октября 2025BABOKSchool

Задачи по программированию на языке C++
По желаниюStepik

Основы программирования на C. Задачи.
По желаниюStepik

Программа курсов: Бэкенд-разработка на Python
По желаниюStepik

PRO Go. Базовый курс по структурам данных
По желаниюStepik

Больше курсов на Хабр Карьере

Информация

Сайт

karuna.group

Дата регистрации

28 мая 2021

Дата основания

13 августа 2015

Численность

201–500 человек

Местоположение

Россия

Блог на Хабре

8 авг 2024 в 14:47

Внутреннее устройство sync.Map, сравнение производительности с map + RWMutex

4.3K

4
+4

23 июл 2024 в 14:36

Ошибки в языке Go — это большая ошибка

19K

96
+96

3 июл 2024 в 14:43

А что если исходные коды программ хранить в бинарном формате?

28K

241
+241

27 мая 2024 в 15:56

Structured concurrency в языке Go

6.3K

19
+19

23 апр 2024 в 15:41

Пишем поиск семантически похожих текстов (или товаров) за полчаса на Go и Postgres (pgVector)

11K

8
+8

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Реклама

ООО «Хабр»

ИНН 7705756279

erid: 2VSb5wN9Qsb

IT в ритейле

Свайпни и узнай, твоё ли это

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr